/**
 * 波浪粒子依赖
 */
class ShaderProgram {

    constructor(holder, options = {}) {

        options = Object.assign({
            antialias: false,
            depthTest: false,
            mousemove: false,
            autosize: true,
            side: 'front',
            vertex: `
    precision highp float;

    attribute vec4 a_position;
    attribute vec4 a_color;

    uniform float u_time;
    uniform vec2 u_resolution;
    uniform vec2 u_mousemove;
    uniform mat4 u_projection;

    varying vec4 v_color;

    void main() {

      gl_Position = u_projection * a_position;
      gl_PointSize = (10.0 / gl_Position.w) * 100.0;

      v_color = a_color;

    }`,
            fragment: `
    precision highp float;

    uniform sampler2D u_texture;
    uniform int u_hasTexture;

    varying vec4 v_color;

    void main() {

      if ( u_hasTexture == 1 ) {

        gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);

      } else {

        gl_FragColor = v_color;

      }

    }`,
            uniforms: {},
            buffers: {},
            camera: {},
            texture: null,
            onUpdate: (() => { }),
            onResize: (() => { }),
        }, options)

        const uniforms = Object.assign({
            time: { type: 'float', value: 0 },
            hasTexture: { type: 'int', value: 0 },
            resolution: { type: 'vec2', value: [0, 0] },
            mousemove: { type: 'vec2', value: [0, 0] },
            projection: { type: 'mat4', value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1] },
        }, options.uniforms)

        const buffers = Object.assign({
            position: { size: 3, data: [] },
            color: { size: 4, data: [] },
        }, options.buffers)

        const camera = Object.assign({
            fov: 60,
            near: 1,
            far: 10000,
            aspect: 1,
            z: 100,
            perspective: true,
        }, options.camera)

        const canvas = document.createElement('canvas')
        const gl = canvas.getContext('webgl', { antialias: options.antialias })

        if (!gl) return false

        this.count = 0
        this.gl = gl
        this.canvas = canvas
        this.camera = camera
        this.holder = holder
        this.onUpdate = options.onUpdate
        this.onResize = options.onResize
        this.data = {}

        holder.appendChild(canvas)

        this.createProgram(options.vertex, options.fragment)

        this.createBuffers(buffers)
        this.createUniforms(uniforms)

        this.updateBuffers()
        this.updateUniforms()

        this.createTexture(options.texture)

        gl.enable(gl.BLEND)
        gl.enable(gl.CULL_FACE)
        gl.blendFunc(gl.SRC_ALPHA, gl.ONE)
        gl[options.depthTest ? 'enable' : 'disable'](gl.DEPTH_TEST)

        if (options.autosize)
            window.addEventListener('resize', e => this.resize(e), false)
        if (options.mousemove)
            window.addEventListener('mousemove', e => this.mousemove(e), false)

        this.resize()

        this.update = this.update.bind(this)
        this.time = { start: performance.now(), old: performance.now() }
        this.update()

    }

    mousemove(e) {

        let x = e.pageX / this.width * 2 - 1
        let y = e.pageY / this.height * 2 - 1

        this.uniforms.mousemove = [x, y]

    }

    resize(e) {

        const holder = this.holder
        const canvas = this.canvas
        const gl = this.gl

        const width = this.width = holder.offsetWidth
        const height = this.height = holder.offsetHeight
        const aspect = this.aspect = width / height
        const dpi = this.dpi = devicePixelRatio

        canvas.width = width * dpi
        canvas.height = height * dpi
        canvas.style.width = width + 'px'
        canvas.style.height = (height - 20) + 'px'

        gl.viewport(0, 0, width * dpi, height * dpi)
        gl.clearColor(0, 0, 0, 0)

        this.uniforms.resolution = [width, height]
        this.uniforms.projection = this.setProjection(aspect)

        this.onResize(width, height, dpi)

    }

    setProjection(aspect) {

        const camera = this.camera

        if (camera.perspective) {

            camera.aspect = aspect

            const fovRad = camera.fov * (Math.PI / 180)
            const f = Math.tan(Math.PI * 0.5 - 0.5 * fovRad)
            const rangeInv = 1.0 / (camera.near - camera.far)

            const matrix = [
                f / camera.aspect, 0, 0, 0,
                0, f, 0, 0,
                0, 0, (camera.near + camera.far) * rangeInv, -1,
                0, 0, camera.near * camera.far * rangeInv * 2, 0
            ]

            matrix[14] += camera.z
            matrix[15] += camera.z

            return matrix

        } else {

            return [
                2 / this.width, 0, 0, 0,
                0, -2 / this.height, 0, 0,
                0, 0, 1, 0,
                -1, 1, 0, 1,
            ]

        }

    }

    createShader(type, source) {

        const gl = this.gl
        const shader = gl.createShader(type)

        gl.shaderSource(shader, source)
        gl.compileShader(shader)

        if (gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {

            return shader

        } else {

            console.log(gl.getShaderInfoLog(shader))
            gl.deleteShader(shader)

        }

    }

    createProgram(vertex, fragment) {

        const gl = this.gl

        const vertexShader = this.createShader(gl.VERTEX_SHADER, vertex)
        const fragmentShader = this.createShader(gl.FRAGMENT_SHADER, fragment)

        const program = gl.createProgram()

        gl.attachShader(program, vertexShader)
        gl.attachShader(program, fragmentShader)
        gl.linkProgram(program)

        if (gl.getProgramParameter(program, gl.LINK_STATUS)) {

            gl.useProgram(program)
            this.program = program

        } else {

            console.log(gl.getProgramInfoLog(program))
            gl.deleteProgram(program)

        }

    }

    createUniforms(data) {

        const gl = this.gl
        const uniforms = this.data.uniforms = data
        const values = this.uniforms = {}

        Object.keys(uniforms).forEach(name => {

            const uniform = uniforms[name]

            uniform.location = gl.getUniformLocation(this.program, 'u_' + name)

            Object.defineProperty(values, name, {
                set: value => {

                    uniforms[name].value = value
                    this.setUniform(name, value)

                },
                get: () => uniforms[name].value
            })

        })

    }

    setUniform(name, value) {

        const gl = this.gl
        const uniform = this.data.uniforms[name]

        uniform.value = value

        switch (uniform.type) {
            case 'int': {
                gl.uniform1i(uniform.location, value)
                break
            }
            case 'float': {
                gl.uniform1f(uniform.location, value)
                break
            }
            case 'vec2': {
                gl.uniform2f(uniform.location, ...value)
                break
            }
            case 'vec3': {
                gl.uniform3f(uniform.location, ...value)
                break
            }
            case 'vec4': {
                gl.uniform4f(uniform.location, ...value)
                break
            }
            case 'mat2': {
                gl.uniformMatrix2fv(uniform.location, false, value)
                break
            }
            case 'mat3': {
                gl.uniformMatrix3fv(uniform.location, false, value)
                break
            }
            case 'mat4': {
                gl.uniformMatrix4fv(uniform.location, false, value)
                break
            }
        }

        // ivec2       : uniform2i,
        // ivec3       : uniform3i,
        // ivec4       : uniform4i,
        // sampler2D   : uniform1i,
        // samplerCube : uniform1i,
        // bool        : uniform1i,
        // bvec2       : uniform2i,
        // bvec3       : uniform3i,
        // bvec4       : uniform4i,

    }

    updateUniforms() {

        const gl = this.gl
        const uniforms = this.data.uniforms

        Object.keys(uniforms).forEach(name => {

            const uniform = uniforms[name]

            this.uniforms[name] = uniform.value

        })

    }

    createBuffers(data) {

        const gl = this.gl
        const buffers = this.data.buffers = data
        const values = this.buffers = {}

        Object.keys(buffers).forEach(name => {

            const buffer = buffers[name]

            buffer.buffer = this.createBuffer('a_' + name, buffer.size)

            Object.defineProperty(values, name, {
                set: data => {

                    buffers[name].data = data
                    this.setBuffer(name, data)

                    if (name == 'position')
                        this.count = buffers.position.data.length / 3

                },
                get: () => buffers[name].data
            })

        })

    }

    createBuffer(name, size) {

        const gl = this.gl
        const program = this.program

        const index = gl.getAttribLocation(program, name)
        const buffer = gl.createBuffer()

        gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
        gl.enableVertexAttribArray(index)
        gl.vertexAttribPointer(index, size, gl.FLOAT, false, 0, 0)

        return buffer

    }

    setBuffer(name, data) {

        const gl = this.gl
        const buffers = this.data.buffers

        if (name == null && !gl.bindBuffer(gl.ARRAY_BUFFER, null)) return

        gl.bindBuffer(gl.ARRAY_BUFFER, buffers[name].buffer)
        gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(data), gl.STATIC_DRAW)

    }

    updateBuffers() {

        const gl = this.gl
        const buffers = this.buffers

        Object.keys(buffers).forEach(name =>
            buffers[name] = buffer.data
        )

        this.setBuffer(null)

    }

    createTexture(src) {

        const gl = this.gl
        const texture = gl.createTexture()

        gl.bindTexture(gl.TEXTURE_2D, texture)
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE, new Uint8Array([0, 0, 0, 0]))

        this.texture = texture

        if (src) {

            this.uniforms.hasTexture = 1
            this.loadTexture(src)

        }

    }

    loadTexture(src) {

        const gl = this.gl
        const texture = this.texture

        const textureImage = new Image()

        textureImage.onload = () => {

            gl.bindTexture(gl.TEXTURE_2D, texture)

            gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, textureImage)

            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR)
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR)

            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE)
            gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE)

            // gl.generateMipmap( gl.TEXTURE_2D )

        }

        textureImage.src = src

    }

    update() {

        const gl = this.gl

        const now = performance.now()
        const elapsed = (now - this.time.start) / 5000
        const delta = now - this.time.old
        this.time.old = now

        this.uniforms.time = elapsed

        if (this.count > 0) {
            gl.clear(gl.COLORBUFFERBIT)
            gl.drawArrays(gl.POINTS, 0, this.count)
        }

        this.onUpdate(delta)

        requestAnimationFrame(this.update)

    }

}

const pointSize = 2.5

const waves = new ShaderProgram(document.querySelector('.waves'), {
    texture: '',
    uniforms: {
        size: { type: 'float', value: pointSize },
        field: { type: 'vec3', value: [0, 0, 0] },
        speed: { type: 'float', value: 5 },
    },
    vertex: `
#define M_PI 3.1415926535897932384626433832795

precision highp float;

attribute vec4 a_position;
attribute vec4 a_color;

uniform float u_time;
uniform float u_size;
uniform float u_speed;
uniform vec3 u_field;
uniform mat4 u_projection;

varying vec4 v_color;

void main() {

  vec3 pos = a_position.xyz;

  pos.y += (
    cos(pos.x / u_field.x * M_PI * 8.0 + u_time * u_speed) +
    sin(pos.z / u_field.z * M_PI * 8.0 + u_time * u_speed)
  ) * u_field.y;

  gl_Position = u_projection * vec4( pos.xyz, a_position.w );
  gl_PointSize = ( u_size / gl_Position.w ) * 100.0;

  v_color = a_color;

}`,
    fragment: `
precision highp float;

uniform sampler2D u_texture;

varying vec4 v_color;

void main() {

  gl_FragColor = v_color * texture2D(u_texture, gl_PointCoord);

}`,
    onResize(w, h, dpi) {

        const position = [], color = []

        const width = 400 * (w / h)
        const depth = 400
        const height = 3
        const distance = 5

        for (let x = 0; x < width; x += distance) {
            for (let z = 0; z < depth; z += distance) {

                position.push(- width / 2 + x, -30, -depth / 2 + z)
                color.push(0, 1 - (x / width) * 1, 0.5 + x / width * 0.5, z / depth)

            }
        }

        this.uniforms.field = [width, height, depth]

        this.buffers.position = position
        this.buffers.color = color

        this.uniforms.size = (h / 400) * pointSize * dpi

    },
})